﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace TvdP
{
    public static class _PrivatePublic
    {
        static object _NullToken = new object();

        public struct _TryFastResult<ToType>
            where ToType : class
        {
            public bool StillNeedsToBeResolved;
            public WeakReference CarrouselSlot;
            public ToType Result;
        }

        public static _TryFastResult<ToType> _TryFast<ToType>(ConditionalWeakTable<object, WeakReference> carrousel, object adaptee)
            where ToType : class
        {
            object resultObj = null;
            var res = new _TryFastResult<ToType>();

            res.StillNeedsToBeResolved = !carrousel.TryGetValue(adaptee, out res.CarrouselSlot) || (resultObj = res.CarrouselSlot.Target) == null;

            if (!res.StillNeedsToBeResolved)
                res.Result = object.ReferenceEquals(resultObj, _NullToken) ? null : (ToType)resultObj;

            return res;
        }

        public static ToType _GetSlow<ToType>(ConditionalWeakTable<object, WeakReference> carrousel, object adaptee, WeakReference carrouselSlot, Func<ToType> generator)
            where ToType : class
        {
            if (carrouselSlot == null)
                carrouselSlot = carrousel.GetValue(adaptee, a => new WeakReference((object)generator() ?? _NullToken));

            var resultObj = carrouselSlot.Target;

            if (resultObj == null)
            {
                //By not locking yet we may create multiple instances in a multithreaded environment but we avoid risk of deadlocks
                //All threads will return same object though.
                resultObj = generator() ?? _NullToken;

                lock (carrouselSlot)
                {
                    //first get strong reference
                    var currentTarget = carrouselSlot.Target;

                    if (currentTarget == null)
                        carrouselSlot.Target = resultObj;
                    else
                        resultObj = (ToType)currentTarget;
                }
            }

            return object.ReferenceEquals(resultObj, _NullToken) ? null : (ToType)resultObj;
        }
    }

    public static partial class Adapters
    {
        internal class CompositeAdapterBuilder
        {
            public CompositeAdapterBuilder()
            {
                GetCarrouselToType = Adapters.MemoizedWithReset((Type toType) => new ConditionalWeakTable<object, WeakReference>()).Item1;

                GetTryFastDelegate = Adapters.MemoizedWithReset((Delegate dgt) => BuildTryFastDelegate(dgt)).Item1;
                GetGetSlowDelegate = Adapters.MemoizedWithReset((Delegate dgt) => BuildGetSlowDelegate(dgt)).Item1;
            }

            Func<Type, ConditionalWeakTable<object, WeakReference>> GetCarrouselToType;


            static MethodInfo _TryFastMethod = typeof(_PrivatePublic).GetMethod("_TryFast");
            static MethodInfo _GetSlowMethod = typeof(_PrivatePublic).GetMethod("_GetSlow");

            /// <summary>
            /// Creates a delegate that tries to get the result for dgt from the carrousel
            /// but does not try to invoke the adapter delegate 
            /// </summary>
            /// <param name="dgt"></param>
            /// <returns></returns>
            public Delegate BuildTryFastDelegate(Delegate dgt)
            {
                var method = dgt.Method;
                var returnType = method.ReturnType;
                var carrousel = GetCarrouselToType(returnType);

                var adapteeParam = Expression.Parameter(typeof(object), "adaptee");

                return
                    Expression.Lambda(
                        Expression.Call(
                            _TryFastMethod.MakeGenericMethod(new[] { returnType }),
                            Expression.Constant(carrousel),
                            adapteeParam
                        ),
                        adapteeParam
                    ).Compile()
                ;
            }
            public Func<Delegate, Delegate> GetTryFastDelegate;

            /// <summary>
            /// Created a delegate that tries to get the result for dgt from the carrousel
            /// and if it can not find it to invoke the adapter to create an instance and add it to the carrousel
            /// </summary>
            /// <param name="dgt"></param>
            /// <returns></returns>
            public Delegate BuildGetSlowDelegate(Delegate dgt)
            {
                var method = dgt.Method;
                var returnType = method.ReturnType;
                var argumentTypes = method.GetParameters().Select(prm => prm.ParameterType).ToArray();
                var carrousel = GetCarrouselToType(returnType);

                var adapteeParam = Expression.Parameter(typeof(object), "adaptee");
                var wrParam = Expression.Parameter(typeof(WeakReference), "carrouselSlot");
                var argPrms = argumentTypes.Select(argt => Expression.Parameter(argt)).ToArray();

                return
                    Expression.Lambda(
                        Expression.Call(
                            _GetSlowMethod.MakeGenericMethod(new[] { returnType }),
                            Expression.Constant(carrousel),
                            adapteeParam,
                            wrParam,
                            Expression.Lambda(
                                Expression.Call(
                                    dgt.Target != null ? Expression.Constant(dgt.Target) : null,
                                    dgt.Method,
                                    argPrms
                                )
                            )
                        ),
                        new[] { adapteeParam, wrParam }.Concat(argPrms).ToArray()
                    ).Compile()
                ;
            }
            public Func<Delegate, Delegate> GetGetSlowDelegate;

            /// <summary>
            /// 
            /// </summary>
            /// <param name="adaptationSteps"></param>
            /// <returns></returns>
            /// <remarks>
            /// General structure for fictitious conversion: o -> A -> C -> R
            ///                                              |         ^    ^
            ///                                              |         |    |
            ///                                              ---> B ---------
            /// Func&lt;object,object&gt; adapter =
            ///     adaptee =>
            ///     {
            ///         var frv3 = TryFastR(adaptee);
            ///         
            ///         if(!frv3.StillNeedsToBeResolved)
            ///             return frv3.Result;
            ///             
            ///         var frv2 = frv3.StillNeedsToBeResolved ? TryFastC(adaptee) : default(_TryFastResult&lt;TC&gt;);
            ///         
            ///         var frv1 = frv3.StillNeedsToBeResolved || frv2.StillNeedsToBeResolved ? TryFastB(adaptee) : default(_TryFastResult&lt;TB&gt;);
            ///         
            ///         var frv0 = frv2.StillNeedsToBeResolved ? TryFastA(adaptee) : default(_TryFastResult&lt;TA&gt;);
            ///         
            ///         var typedAdaptee = (TFrom)adaptee;
            ///         
            ///         if(frv0.StillNeedsToBeResolved)
            ///             frv0.Result = GetSlowA(typedAdaptee, frv0.CarrouselSlot, typedAdaptee);
            ///             
            ///         if(frv1.StillNeedsToBeResolved)
            ///             frv1.Result = GetSlowB(typedAdaptee, frv1.CarrouselSlot, typedAdaptee);
            ///             
            ///         if(frv2.StillNeedsToBeResolved)
            ///             frv2.Result = GetSlowC(typedAdaptee, frv2.CarrouselSlot, frv0.Result, frv1.Result);
            ///             
            ///         if(frv3.StillNeedsToBeResolved)
            ///             frv3.Result = GetSlowR(typedAdaptee, frv1.CarrouselSlot, frv2.Result, frv1.Result);
            ///             
            ///         return frv3.Result;
            ///     }
            /// </remarks>
            public Func<object, object> BuildAdapter(IEnumerable<AdaptationPathFinder.Step> adaptationSteps)
            {
                if (!adaptationSteps.Any())
                    return adaptee => adaptee;

                var fastResultVariables =
                    adaptationSteps
                        .Select(
                            (step, ix) => 
                                new { 
                                    returnType = step.ReturnType,
                                    variable = 
                                        Expression.Variable(
                                            typeof(_PrivatePublic._TryFastResult<>).MakeGenericType(step.ReturnType),
                                            "frv" + ix
                                        ) 
                                }
                        )
                        .ToDictionary(
                            n => n.returnType, 
                            n => n.variable
                        )
                ;

                var adapteeParam = Expression.Parameter(typeof(object), "adaptee");

                //Map of dependency to what depends on it.
                var dependencyOfMap = 
                    adaptationSteps
                        .SelectMany(
                            step => 
                                step.ArgumentTypes
                                    .Select(
                                        stepArgT => 
                                            new { 
                                                dependent = step.ReturnType, 
                                                dependency = stepArgT 
                                            }
                                   )
                        )
                        .ToLookup(
                            n => n.dependency, 
                            n => n.dependent
                        )
                ;


                //All lines of form similar to (only first):
                ///         var frv0 = TryFastR(adaptee);
                //or (all others):         
                ///         var frv1 = frv3.StillNeedsToBeResolved || frv2.StillNeedsToBeResolved ? TryFastB(adaptee) : default(_TryFastResult&lt;TB&gt;);
                var variableInitializations =
                    adaptationSteps.Reverse().Select(
                        (step, ix) =>
                        {
                            var tryFastInvoke =
                                (Expression)Expression.Invoke(
                                    Expression.Constant(GetTryFastDelegate(step.Delegate)),
                                    adapteeParam
                                )
                            ;

                            var initializedVar = fastResultVariables[step.ReturnType];

                            return
                                (Expression)Expression.Assign(
                                    initializedVar,
                                    ix == 0 ? tryFastInvoke :
                                        Expression.Condition(
                                            dependencyOfMap[step.ReturnType]
                                                .Select(dependent => (Expression)Expression.Field(fastResultVariables[dependent], "StillNeedsToBeResolved"))
                                                .Aggregate((exp1, exp2) => Expression.OrElse(exp1, exp2)),
                                            tryFastInvoke,
                                            Expression.Default(initializedVar.Type)
                                        )
                                )
                            ;
                        }
                    )
                    .ToList()
                ;


                var fromType = adaptationSteps.First().ArgumentTypes[0];
                var adapteeTypedVariable = Expression.Variable(fromType, "typedAdaptee");

                //map of required type to expression that gets the value from it's apropriate source 'frvX.Result' for all except 'fromType' which has 'typedAdaptee'
                var inputParameterExpressionsMap =
                    new[] { new { type = fromType, expression = (Expression)adapteeTypedVariable } }
                    .Concat(fastResultVariables.Select(kvp => new { type = kvp.Key, expression = (Expression)Expression.Field(kvp.Value, "Result") }))
                    .ToDictionary(n => n.type, n => n.expression)
                ;


                //Creation lines (creation if not found in carrousel) like:
                ///         if(frv2.StillNeedsToBeResolved)
                ///             frv2.Result = GetSlowC(typedAdaptee, frv2.CarrouselSlot, frv0.Result, frv1.Result);
                var creations =
                    adaptationSteps.Select(
                        step =>
                        {
                            var fastResultVar = fastResultVariables[step.ReturnType];
                            var sDgt = GetGetSlowDelegate(step.Delegate);

                            return
                                (Expression)Expression.IfThen(
                                    Expression.Field(fastResultVar, "StillNeedsToBeResolved"),
                                    Expression.Assign(
                                        Expression.Field(fastResultVar, "Result"),
                                        Expression.Invoke(
                                            Expression.Constant(sDgt),
                                            new Expression[] {
                                                adapteeTypedVariable,
                                                Expression.Field(fastResultVar, "CarrouselSlot")
                                            }.Concat(
                                                step.ArgumentTypes.Select(argt => inputParameterExpressionsMap[argt])
                                            )
                                        )
                                    )
                                )
                            ;
                        }
                    )
                ;

                var toTypeFastResultVariable = fastResultVariables[adaptationSteps.Last().ReturnType];
                var returnLabel = Expression.Label(typeof(object));
               
                var lambdaBody =
                    new [] {
                        variableInitializations.Take(1),
                        new [] {
                            Expression.IfThen(                                                                              //Shortcut inserted after trying to get endresult from carrousel, like:                               
                                Expression.Not(Expression.Field(toTypeFastResultVariable, "StillNeedsToBeResolved")),       ///         if(!frv3.StillNeedsToBeResolved)
                                Expression.Return(returnLabel, Expression.Field(toTypeFastResultVariable, "Result"))        ///             return frv3.Result;
                            )
                        },
                        variableInitializations.Skip(1),
                        new[] { Expression.Assign(adapteeTypedVariable, Expression.Convert(adapteeParam, fromType)) },      //var typedAdaptee = (TFrom)adaptee;
                        creations,
                        new Expression[] { Expression.Label(returnLabel, Expression.Field(toTypeFastResultVariable, "Result")) } //return frv3.Result;
                    }
                    .SelectMany(a => a)
                ;

                return
                    Expression.Lambda<Func<object, object>>(
                        Expression.Block(
                            typeof(object),
                            new[] { adapteeTypedVariable }.Concat(fastResultVariables.Select(frv => frv.Value)),
                            lambdaBody
                            
                        ),
                        adapteeParam
                    ).Compile()
                ;
            }
        }
    }
}
